﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;

namespace Kyoh.Lib.Text
{
    /// <summary>文字列の尤度計算を行います。</summary>
    [Serializable]
    public class StringLikelihood : ISerializable
    {
        private readonly Dictionary<UnicodeGroup, decimal> instruction = new Dictionary<UnicodeGroup, decimal>();

        /// <summary><see cref="StringLikelihood"/> クラスの新しいインスタンスを初期化します。</summary>
        public StringLikelihood()
        {
            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
                this.instruction.Add(group, 0M);
        }

        /// <summary><see cref="StringLikelihood"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="info">転送元または転送先に関するコンテキスト情報を含んでいる <see cref="StreamingContext"/>。</param>
        /// <param name="context">スローされている例外に関するシリアル化済みオブジェクト データを保持している <see cref="SerializationInfo"/>。</param>
        protected StringLikelihood(SerializationInfo info, StreamingContext context)
        {
            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
                this.instruction.Add(group, info.GetDecimal(Enum.GetName(typeof(UnicodeGroup), group)));
        }

        /// <summary>シリアライズするデータを取得します。</summary>
        /// <param name="info">転送元または転送先に関するコンテキスト情報を含んでいる <see cref="StreamingContext"/>。</param>
        /// <param name="context">スローされている例外に関するシリアル化済みオブジェクト データを格納する <see cref="SerializationInfo"/>。</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
                info.AddValue(Enum.GetName(typeof(UnicodeGroup), group), this.instruction[group]);
        }

        private void InitInstruction()
        {
            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
                this.instruction[group] = 0M;
        }

        /// <summary>文字列の尤度を計算します。</summary>
        /// <param name="str">処理対象の <see cref="System.String"/>。</param>
        /// <param name="normalize">計算した尤度を、 <paramref name="str"/> の長さによって正規化する場合は <c>true</c>。それ以外の場合は <c>false</c>。</param>
        /// <returns>算出した尤度を表す <see cref="System.Decimal"/>。</returns>
        public decimal CalcLikelihood(string str, bool normalize = false)
        {
            decimal instLike = 0;
            foreach (UnicodeGroup group in UnicodeGroups.GetGroups(str))
                instLike += this.instruction[group];
            if (normalize)
                return (instLike) / str.Length;
            else
                return instLike;
        }

        private const decimal likelihoodDelta = 1M;
        /// <summary>尤度算出に用いる文字毎の尤度値を設定します。</summary>
        /// <param name="reader">尤度算出に用いるサンプル文字列を含む <see cref="TextReader"/>。</param>
        /// <param name="length"><paramref name="reader"/> の長さ。</param>
        public void SetInstruction(TextReader reader, long length)
        {
            decimal count = Enum.GetValues(typeof(UnicodeGroup)).Length;
            string line;
            decimal total = 0;
            this.InitInstruction();
            // すべての文字について、groupを求めて、group毎の教師値をプラスしていく
            while (null != (line = reader.ReadLine()))
            {
                foreach (UnicodeGroup group in UnicodeGroups.GetGroups(line))
                {
                    this.instruction[group] += likelihoodDelta;
                    total += likelihoodDelta;
                }
            }

            // 値の正規化
            decimal mean = total / count;
            decimal deviation = 0M;
            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
            {
                if (this.instruction[group] != 0)
                {
                    this.instruction[group] -= mean;
                    deviation += this.instruction[group] * this.instruction[group];
                }
                else
                    this.instruction[group] = -total;
            }
            deviation /= count;
            deviation = new decimal(Math.Sqrt(decimal.ToDouble(deviation)));

            foreach (UnicodeGroup group in Enum.GetValues(typeof(UnicodeGroup)))
                this.instruction[group] /= deviation;
        }

        /// <summary>尤度算出に用いる文字毎の尤度値を設定します。</summary>
        /// <param name="stream">尤度算出に用いるサンプル文字列を含む <see cref="Stream"/>。</param>
        public void SetInstruction(Stream stream)
        {
            using (TextReader reader = new StreamReader(stream))
                this.SetInstruction(reader, stream.Length);
        }

        /// <summary>尤度算出に用いる文字毎の尤度値を設定します。</summary>
        /// <param name="fileName">尤度算出に用いるサンプル文字列を含むファイルのフルパス。</param>
        public void SetInstruction(string fileName)
        {
            using (var stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
                this.SetInstruction(stream);
        }
    }
}
